/**
 * Base class from Ext.ux.TabReorderer.
 */
Ext.define('Ext.ux.BoxReorderer', {
	mixins : {
		observable : 'Ext.util.Observable'
	},

	/**
	 * @cfg {String} itemSelector A {@link Ext.DomQuery DomQuery} selector which
	 *      identifies the encapsulating elements of child Components which
	 *      participate in reordering.
	 */
	itemSelector : '.x-box-item',

	/**
	 * @cfg {Mixed} animate If truthy, child reordering is animated so that
	 *      moved boxes slide smoothly into position. If this option is numeric,
	 *      it is used as the animation duration in milliseconds.
	 */
	animate : 100,

	constructor : function() {
		this.addEvents(
				/**
				 * @event StartDrag Fires when dragging of a child Component
				 *        begins.
				 * @param {Ext.ux.BoxReorderer}
				 *            this
				 * @param {Ext.container.Container}
				 *            container The owning Container
				 * @param {Ext.Component}
				 *            dragCmp The Component being dragged
				 * @param {Number}
				 *            idx The start index of the Component being
				 *            dragged.
				 */
				'StartDrag',
				/**
				 * @event Drag Fires during dragging of a child Component.
				 * @param {Ext.ux.BoxReorderer}
				 *            this
				 * @param {Ext.container.Container}
				 *            container The owning Container
				 * @param {Ext.Component}
				 *            dragCmp The Component being dragged
				 * @param {Number}
				 *            startIdx The index position from which the
				 *            Component was initially dragged.
				 * @param {Number}
				 *            idx The current closest index to which the
				 *            Component would drop.
				 */
				'Drag',
				/**
				 * @event ChangeIndex Fires when dragging of a child Component
				 *        causes its drop index to change.
				 * @param {Ext.ux.BoxReorderer}
				 *            this
				 * @param {Ext.container.Container}
				 *            container The owning Container
				 * @param {Ext.Component}
				 *            dragCmp The Component being dragged
				 * @param {Number}
				 *            startIdx The index position from which the
				 *            Component was initially dragged.
				 * @param {Number}
				 *            idx The current closest index to which the
				 *            Component would drop.
				 */
				'ChangeIndex',
				/**
				 * @event Drop Fires when a child Component is dropped at a new
				 *        index position.
				 * @param {Ext.ux.BoxReorderer}
				 *            this
				 * @param {Ext.container.Container}
				 *            container The owning Container
				 * @param {Ext.Component}
				 *            dragCmp The Component being dropped
				 * @param {Number}
				 *            startIdx The index position from which the
				 *            Component was initially dragged.
				 * @param {Number}
				 *            idx The index at which the Component is being
				 *            dropped.
				 */
				'Drop');
		this.mixins.observable.constructor.apply(this, arguments);
	},

	init : function(container) {
		var me = this;

		me.container = container;

		// Set our animatePolicy to animate the start position (ie x for HBox, y
		// for VBox)
		me.animatePolicy = {};
		me.animatePolicy[container.getLayout().names.x] = true;

		// Initialize the DD on first layout, when the innerCt has been created.
		me.container.on({
					scope : me,
					boxready : me.afterFirstLayout,
					beforedestroy : me.onContainerDestroy
				});
	},

	/**
	 * @private Clear up on Container destroy
	 */
	onContainerDestroy : function() {
		var dd = this.dd;
		if (dd) {
			dd.unreg();
			this.dd = null;
		}
	},

	afterFirstLayout : function() {
		var me = this, layout = me.container.getLayout(), names = layout.names, dd;

		// Create a DD instance. Poke the handlers in.
		dd = me.dd = Ext.create('Ext.dd.DD', layout.innerCt, me.container.id
						+ '-reorderer');
		Ext.apply(dd, {
					animate : me.animate,
					reorderer : me,
					container : me.container,
					getDragCmp : this.getDragCmp,
					clickValidator : Ext.Function.createInterceptor(
							dd.clickValidator, me.clickValidator, me, false),
					onMouseDown : me.onMouseDown,
					startDrag : me.startDrag,
					onDrag : me.onDrag,
					endDrag : me.endDrag,
					getNewIndex : me.getNewIndex,
					doSwap : me.doSwap,
					findReorderable : me.findReorderable
				});

		// Decide which dimension we are measuring, and which measurement metric
		// defines
		// the *start* of the box depending upon orientation.
		dd.dim = names.width;
		dd.startAttr = names.beforeX;
		dd.endAttr = names.afterX;
	},

	getDragCmp : function(e) {
		return this.container.getChildByElement(e.getTarget(this.itemSelector,
				10));
	},

	// check if the clicked component is reorderable
	clickValidator : function(e) {
		var cmp = this.getDragCmp(e);

		// If cmp is null, this expression MUST be coerced to boolean so that
		// createInterceptor is able to test it against false
		return !!(cmp && cmp.reorderable !== false);
	},

	onMouseDown : function(e) {
		var me = this, container = me.container, containerBox, cmpEl, cmpBox;

		// Ascertain which child Component is being mousedowned
		me.dragCmp = me.getDragCmp(e);
		if (me.dragCmp) {
			cmpEl = me.dragCmp.getEl();
			me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);

			// Start position of dragged Component
			cmpBox = cmpEl.getBox();

			// Last tracked start position
			me.lastPos = cmpBox[this.startAttr];

			// Calculate constraints depending upon orientation
			// Calculate offset from mouse to dragEl position
			containerBox = container.el.getBox();
			if (me.dim === 'width') {
				me.minX = containerBox.left;
				me.maxX = containerBox.right - cmpBox.width;
				me.minY = me.maxY = cmpBox.top;
				me.deltaX = e.getPageX() - cmpBox.left;
			} else {
				me.minY = containerBox.top;
				me.maxY = containerBox.bottom - cmpBox.height;
				me.minX = me.maxX = cmpBox.left;
				me.deltaY = e.getPageY() - cmpBox.top;
			}
			me.constrainY = me.constrainX = true;
		}
	},

	startDrag : function() {
		var me = this, dragCmp = me.dragCmp;

		if (dragCmp) {
			// For the entire duration of dragging the *Element*, defeat any
			// positioning and animation of the dragged *Component*
			dragCmp.setPosition = Ext.emptyFn;
			dragCmp.animate = false;

			// Animate the BoxLayout just for the duration of the drag
			// operation.
			if (me.animate) {
				me.container.getLayout().animatePolicy = me.reorderer.animatePolicy;
			}
			// We drag the Component element
			me.dragElId = dragCmp.getEl().id;
			me.reorderer.fireEvent('StartDrag', me, me.container, dragCmp,
					me.curIndex);
			// Suspend events, and set the disabled flag so that the mousedown
			// and mouseup events
			// that are going to take place do not cause any other UI
			// interaction.
			dragCmp.suspendEvents();
			dragCmp.disabled = true;
			dragCmp.el.setStyle('zIndex', 100);
		} else {
			me.dragElId = null;
		}
	},

	/**
	 * @private Find next or previous reorderable component index.
	 * @param {Number}
	 *            newIndex The initial drop index.
	 * @return {Number} The index of the reorderable component.
	 */
	findReorderable : function(newIndex) {
		var me = this, items = me.container.items, newItem;

		if (items.getAt(newIndex).reorderable === false) {
			newItem = items.getAt(newIndex);
			if (newIndex > me.startIndex) {
				while (newItem && newItem.reorderable === false) {
					newIndex++;
					newItem = items.getAt(newIndex);
				}
			} else {
				while (newItem && newItem.reorderable === false) {
					newIndex--;
					newItem = items.getAt(newIndex);
				}
			}
		}

		newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);

		if (items.getAt(newIndex).reorderable === false) {
			return -1;
		}
		return newIndex;
	},

	/**
	 * @private Swap 2 components.
	 * @param {Number}
	 *            newIndex The initial drop index.
	 */
	doSwap : function(newIndex) {
		var me = this, items = me.container.items, container = me.container, wasRoot = me.container._isLayoutRoot, orig, dest, tmpIndex, temp;

		newIndex = me.findReorderable(newIndex);

		if (newIndex === -1) {
			return;
		}

		me.reorderer.fireEvent('ChangeIndex', me, container, me.dragCmp,
				me.startIndex, newIndex);
		orig = items.getAt(me.curIndex);
		dest = items.getAt(newIndex);
		items.remove(orig);
		tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
		items.insert(tmpIndex, orig);
		items.remove(dest);
		items.insert(me.curIndex, dest);

		// Make the Box Container the topmost layout participant during the
		// layout.
		container._isLayoutRoot = true;
		container.updateLayout();
		container._isLayoutRoot = wasRoot;
		me.curIndex = newIndex;
	},

	onDrag : function(e) {
		var me = this, newIndex;

		newIndex = me.getNewIndex(e.getPoint());
		if ((newIndex !== undefined)) {
			me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp,
					me.startIndex, me.curIndex);
			me.doSwap(newIndex);
		}

	},

	endDrag : function(e) {
		if (e) {
			e.stopEvent();
		}
		var me = this, layout = me.container.getLayout(), temp;

		if (me.dragCmp) {
			delete me.dragElId;

			// Reinstate the Component's positioning method after mouseup, and
			// allow the layout system to animate it.
			delete me.dragCmp.setPosition;
			me.dragCmp.animate = true;

			// Ensure the lastBox is correct for the animation system to restore
			// to when it creates the "from" animation frame
			me.dragCmp.lastBox[layout.names.x] = me.dragCmp.getPosition(true)[layout.names.widthIndex];

			// Make the Box Container the topmost layout participant during the
			// layout.
			me.container._isLayoutRoot = true;
			me.container.updateLayout();
			me.container._isLayoutRoot = undefined;

			// Attempt to hook into the afteranimate event of the drag Component
			// to call the cleanup
			temp = Ext.fx.Manager.getFxQueue(me.dragCmp.el.id)[0];
			if (temp) {
				temp.on({
							afteranimate : me.reorderer.afterBoxReflow,
							scope : me
						});
			}
			// If not animated, clean up after the mouseup has happened so that
			// we don't click the thing being dragged
			else {
				Ext.Function.defer(me.reorderer.afterBoxReflow, 1, me);
			}

			if (me.animate) {
				delete layout.animatePolicy;
			}
			me.reorderer.fireEvent('drop', me, me.container, me.dragCmp,
					me.startIndex, me.curIndex);
		}
	},

	/**
	 * @private Called after the boxes have been reflowed after the drop.
	 *          Re-enabled the dragged Component.
	 */
	afterBoxReflow : function() {
		var me = this;
		me.dragCmp.el.setStyle('zIndex', '');
		me.dragCmp.disabled = false;
		me.dragCmp.resumeEvents();
	},

	/**
	 * @private Calculate drop index based upon the dragEl's position.
	 */
	getNewIndex : function(pointerPos) {
		var me = this, dragEl = me.getDragEl(), dragBox = Ext.fly(dragEl)
				.getBox(), targetEl, targetBox, targetMidpoint, i = 0, it = me.container.items.items, ln = it.length, lastPos = me.lastPos;

		me.lastPos = dragBox[me.startAttr];

		for (; i < ln; i++) {
			targetEl = it[i].getEl();

			// Only look for a drop point if this found item is an item
			// according to our selector
			if (targetEl.is(me.reorderer.itemSelector)) {
				targetBox = targetEl.getBox();
				targetMidpoint = targetBox[me.startAttr]
						+ (targetBox[me.dim] >> 1);
				if (i < me.curIndex) {
					if ((dragBox[me.startAttr] < lastPos)
							&& (dragBox[me.startAttr] < (targetMidpoint - 5))) {
						return i;
					}
				} else if (i > me.curIndex) {
					if ((dragBox[me.startAttr] > lastPos)
							&& (dragBox[me.endAttr] > (targetMidpoint + 5))) {
						return i;
					}
				}
			}
		}
	}
});
